﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UMLSynthesizer.CSharpAnalyzer.Helpers;
using UMLSynthesizer.LangAnalyzer;
using UMLSynthesizer.LangAnalyzer.Elements;
using UMLSynthesizer.LangAnalyzer.ExtensionMethods;

namespace UMLSynthesizer.CSharpAnalyzer.Elements
{
    class CSharpProjectElement : IProjectElement
    {
        #region Fields

        Compilation compilation;
        CSharpElementContainerHelper containerHelper;
        List<ITypeBase> externalTypes;

        #endregion

        #region Constructor

        /// <summary>
        /// The constructor of the project
        /// </summary>
        /// <param name="projectName">The project name</param>
        /// <param name="compilation">The compilation of the project</param>
        public CSharpProjectElement(Compilation compilation)
        {
            if (compilation == null)
                throw new ArgumentNullException("compilation");

            this.Identifier = "Root project";
            this.compilation = compilation;

            this.externalTypes = new List<ITypeBase>();
            
            this.containerHelper = new CSharpElementContainerHelper(this,
                () => from tree in compilation.SyntaxTrees.ToArray()
                      select tree.GetRoot(),
                () => new KeyValuePair<int, SyntaxNode>[0]);

            this.MetricDictionary = new Dictionary<LanguageAnalyzer.Metrics, double>();
        }

        #endregion

        #region Properties

        public IEnumerable<string> SourceFiles
        {
            get
            {
                return (from tree in compilation.SyntaxTrees.ToArray()
                        orderby tree.FilePath
                        select tree.FilePath).Distinct();
            }
        }
        public string SourceLanguage { get { return "C#"; } }
        public IEnumerable<string> AssemblyReferences
        {
            get
            {
                return from assemblyIdentity in compilation.ReferencedAssemblyNames
                       select assemblyIdentity.Name;
            }
        }

        public string Identifier { get; set; }
        public ElementKinds Kind { get { return ElementKinds.Project; } }
        public IElementBase ParentElement { get { return null; } }
        public IProjectElement ProjectOwner { get { return this; } }

        public IEnumerable<IClassElement> ContainedClasses { get { return containerHelper.ContainedClasses; } }
        public IEnumerable<IDelegateElement> ContainedDelegates { get { return containerHelper.ContainedDelegates; } }
        public IEnumerable<IEnumElement> ContainedEnums { get { return containerHelper.ContainedEnums; } }
        public IEnumerable<IFieldElement> ContainedFields { get { return containerHelper.ContainedFields; } }
        public IEnumerable<IInterfaceElement> ContainedInterfaces { get { return containerHelper.ContainedInterfaces; } }
        public IEnumerable<IMethodElement> ContainedMethods { get { return containerHelper.ContainedMethods; } }
        public IEnumerable<INamespaceElement> ContainedNamespaces { get { return containerHelper.ContainedNamespaces; } }
        public IEnumerable<IPropertyElement> ContainedProperties { get { return containerHelper.ContainedProperties; } }
        public IEnumerable<IStructElement> ContainedStructs { get { return containerHelper.ContainedStructs; } }

        #endregion

        #region Public interface

        public IElementBase GetElementReference(ExpressionSyntax expressionSyntax)
        {
            var symbol = compilation.GetSemanticModel(expressionSyntax.SyntaxTree)
                .GetSymbolInfo(expressionSyntax).Symbol;
            if(symbol == null)
                return null;
            return this.GetContainedElement(symbol.ContainingSymbol.ToString() + '.' + symbol.Name.ToString());
        }
        public ITypeBase GetTypeReference(TypeSyntax typeSyntax)
        {
            ITypeSymbol typeSymbol = compilation.GetSemanticModel(typeSyntax.SyntaxTree)
                .GetTypeInfo(typeSyntax).Type;

            //I don't know why sometimes, even if the symbol exists, typeSymbol is
            //assigned null. I thing this may be a Roslyn bug.
            if (typeSymbol == null)
                return null;
            return GetTypeReference(typeSymbol);
        }
        public ITypeBase GetTypeReference(ITypeSymbol typeSymbol)
        {
            //try with an internal type
            var element = this.GetContainedElement(typeSymbol.ToString()) as ITypeBase;
            if (element != null)
                return element;

            //try with an array type
            if (typeSymbol.TypeKind == TypeKind.ArrayType)
                return new CSharpArrayType(this, typeSymbol as IArrayTypeSymbol);

            //typeSymbol without name (ex. var)
            if (typeSymbol.Name == "")
                return null;

            //get an external type reference
            string fullPath = typeSymbol.Name;
            var parentContainer = typeSymbol.ContainingSymbol;
            while (parentContainer != null && parentContainer.Name != "")
            {
                fullPath = parentContainer.Name + '.' + fullPath;
                parentContainer = parentContainer.ContainingSymbol;
            }

            element = (from externalElement in externalTypes
                       where externalElement.Identifier == fullPath
                       select externalElement).FirstOrDefault();
            if (element == null)
            {
                element = new CSharpExternalType(fullPath);
                externalTypes.Add(element);
            }

            return element;
        }

        public Dictionary<LanguageAnalyzer.Metrics, double> MetricDictionary { get; private set; }

        #endregion
    }
}
